OpenGL provides a mechanism called
vertex buffer objects (often known as VBOs) whereby
you give it ownership of a set of vertices (and/or indices), allowing you
to free up CPU memory and avoid frequent CPU-to-GPU transfers. Using VBOs
is such a highly recommended practice that I considered using them even in
the HelloArrow sample. Going forward, all sample code in this book will
use VBOs.Let’s walk through the steps required to add
VBOs to Touch Cone. First, remove these two lines from the
RenderingEngine class declaration:
vector<Vertex> m_coneVertices;
vector<GLubyte> m_coneIndices;
They’re no longer needed because the vertex
data will be stored in OpenGL memory. You do, however, need to store the
handles to the vertex buffer objects. Object handles in OpenGL are of type
GLuint. So, add these two lines to the class
declaration:
GLuint m_vertexBuffer;
GLuint m_indexBuffer;
The vertex generation code in the
Initialize method stays the same except that you should
use a temporary variable rather than a class member for storing the vertex
list. Specifically, replace this snippet:
m_coneVertices.resize(vertexCount);
vector<Vertex>::iterator vertex = m_coneVertices.begin();
// Cone's body
for (float theta = 0; vertex != m_coneVertices.end() - 1; theta += dtheta) {
...
m_coneIndices.resize(m_bodyIndexCount + m_diskIndexCount);
vector<GLubyte>::iterator index = m_coneIndices.begin();
with this:
vector<Vertex> coneVertices(vertexCount);
vector<Vertex>::iterator vertex = coneVertices.begin();
// Cone's body
for (float theta = 0; vertex != coneVertices.end() - 1; theta += dtheta) {
...
vector<GLubyte> coneIndices(m_bodyIndexCount + m_diskIndexCount);
vector<GLubyte>::iterator index = coneIndices.begin();
Next you need to create the vertex buffer
objects and populate them. This is done with some OpenGL function calls
that follow the same Gen/Bind pattern that you’re already using for
framebuffer objects. The Gen/Bind calls for VBOs are shown here (don’t add
these snippets to the class just yet):
void glGenBuffers(GLsizei count, GLuint* handles);
void glBindBuffer(GLenum target, GLuint handle);
glGenBuffers generates a
list of nonzero handles. count specifies the
desired number of handles; handles points to a
preallocated list. In this book we often generate only one handle at a
time, so be aware that the glGen* functions can also be
used to efficiently generate several handles at once.
The glBindBuffer function
attaches a VBO to one of two binding points specified with the
target parameter. The legal values for
target are
GL_ELEMENT_ARRAY_BUFFER (used for indices) and
GL_ARRAY_BUFFER (used for vertices).
Populating a VBO that’s already attached to one
of the two binding points is accomplished with this function
call:
void glBufferData(GLenum target, GLsizeiptr size,
const GLvoid* data, GLenum usage);
target is the same as it
is in glBindBuffer, size is the
number of bytes in the VBO (GLsizeiptr is a typedef of
int), data points to the source
memory, and usage gives a hint to OpenGL about how
you intend to use the VBO. The possible values for
usage are as follows:
GL_STATIC_DRAW
This is what we’ll commonly use in this
book; it tells OpenGL that the buffer never changes.
GL_DYNAMIC_DRAW
This tells OpenGL that the buffer will be
periodically updated using
glBufferSubData.
GL_STREAM_DRAW (ES 2.0 only)
This tells OpenGL that the buffer will be
frequently updated (for example, once per frame) with
glBufferSubData.
To modify the contents of an existing VBO, you
can use glBufferSubData:
void glBufferSubData(GLenum target, GLintptr offset,
GLsizeiptr size, const GLvoid* data);
The only difference between this and
glBufferData is the offset
parameter, which specifies a number of bytes from the start of the VBO.
Note that glBufferSubDataglBufferData. should be used only to update
a VBO that has previously been initialized with
We won’t be using
glBufferSubData in any of the samples in this book.
Frequent updates with glBufferSubData should be avoided
for best performance, but in many scenarios it can be very useful.
Getting back to Touch Cone, let’s add code to
create and populate the VBOs near the end of the
Initialize method:
// Create the VBO for the vertices.
glGenBuffers(1, &m_vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, m_vertexBuffer);
glBufferData(GL_ARRAY_BUFFER,
coneVertices.size() * sizeof(coneVertices[0]),
&coneVertices[0],
GL_STATIC_DRAW);
// Create the VBO for the indices.
glGenBuffers(1, &m_indexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
coneIndices.size() * sizeof(coneIndices[0]),
&coneIndices[0],
GL_STATIC_DRAW);
Before showing you how to use VBOs for
rendering, let me refresh your memory on the gl*Pointer
functions that you’ve been using in the Render
method:
// ES 1.1
glVertexPointer(3, GL_FLOAT, stride, pCoords);
glColorPointer(4, GL_FLOAT, stride, pColors);
// ES 2.0
glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, stride, pCoords);
glVertexAttribPointer(colorSlot, 4, GL_FLOAT, GL_FALSE, stride, pColors);
The formal declarations for these functions
look like this:
// From <OpenGLES/ES1/gl.h>
void glVertexPointer(GLint size, GLenum type, GLsizei stride, const GLvoid* pointer);
void glColorPointer(GLint size, GLenum type, GLsizei stride, const GLvoid* pointer);
void glNormalPointer(GLenum type, GLsizei stride, const GLvoid* pointer);
void glTexCoordPointer(GLint size, GLenum type, GLsizei stride, const GLvoid* pointer);
void glPointSizePointerOES(GLenum type, GLsizei stride, const GLvoid* pointer);
// From <OpenGLES/ES2/gl.h>
void glVertexAttribPointer(GLuint attributeIndex, GLint size, GLenum type,
GLboolean normalized, GLsizei stride,
const GLvoid* pointer);
The size parameter in
all these functions controls the number of vector components per
attribute. The stride parameter is
the number of bytes between vertices. The pointer
parameter is the one to watch out for—when no VBOs are bound (that is, the
current VBO binding is zero), it’s a pointer to CPU memory; when a VBO is
bound to GL_ARRAY_BUFFER, it changes meaning and
becomes a byte offset rather than a pointer.
The gl*Pointer functions are
used to set up vertex attributes, but recall that indices are submitted
through the last argument of glDrawElements. Here’s the
formal declaration of glDrawElements:
void glDrawElements(GLenum topology, GLsizei count, GLenum type, GLvoid* indices);
indices is another
“chameleon” parameter. When a nonzero VBO is bound to GL_ELEMENT_ARRAY_BUFFER, it’s a byte
offset; otherwise, it’s a pointer to CPU memory.
Note:
The shape-shifting aspect of
gl*Pointer and glDrawElements is
an indicator of how OpenGL has grown organically through the years; if
the API were designed from scratch, perhaps these functions wouldn’t be
so overloaded.
To see glDrawElements and
gl*Pointer being used with VBOs in Touch Cone, check
out the Render method in Example 1.
Example 1. RenderingEngine1::Render with vertex buffer objects
void RenderingEngine1::Render() const { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glPushMatrix(); glRotatef(m_rotationAngle, 0, 0, 1); glScalef(m_scale, m_scale, m_scale);
const GLvoid* colorOffset = (GLvoid*) sizeof(vec3); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBuffer); glBindBuffer(GL_ARRAY_BUFFER, m_vertexBuffer); glVertexPointer(3, GL_FLOAT, sizeof(Vertex), 0); glColorPointer(4, GL_FLOAT, sizeof(Vertex), colorOffset); glEnableClientState(GL_VERTEX_ARRAY);
const GLvoid* bodyOffset = 0; const GLvoid* diskOffset = (GLvoid*) m_bodyIndexCount;
glEnableClientState(GL_COLOR_ARRAY); glDrawElements(GL_TRIANGLES, m_bodyIndexCount, GL_UNSIGNED_BYTE, bodyOffset); glDisableClientState(GL_COLOR_ARRAY); glColor4f(1, 1, 1, 1); glDrawElements(GL_TRIANGLES, m_diskIndexCount, GL_UNSIGNED_BYTE, diskOffset);
glDisableClientState(GL_VERTEX_ARRAY); glPopMatrix(); }
|
Example 2 shows
the ES 2.0 variant. From 30,000 feet, it basically does the same thing,
even though many of the actual OpenGL calls are different.
Example 2. RenderingEngine2::Render with vertex buffer objects
void RenderingEngine2::Render() const { GLuint positionSlot = glGetAttribLocation(m_simpleProgram, "Position"); GLuint colorSlot = glGetAttribLocation(m_simpleProgram, "SourceColor");
mat4 rotation = mat4::Rotate(m_rotationAngle); mat4 scale = mat4::Scale(m_scale); mat4 translation = mat4::Translate(0, 0, -7); GLint modelviewUniform = glGetUniformLocation(m_simpleProgram, "Modelview"); mat4 modelviewMatrix = scale * rotation * translation;
GLsizei stride = sizeof(Vertex); const GLvoid* colorOffset = (GLvoid*) sizeof(vec3);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUniformMatrix4fv(modelviewUniform, 1, 0, modelviewMatrix.Pointer());
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBuffer); glBindBuffer(GL_ARRAY_BUFFER, m_vertexBuffer); glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, stride, 0); glVertexAttribPointer(colorSlot, 4, GL_FLOAT, GL_FALSE, stride, colorOffset); glEnableVertexAttribArray(positionSlot); const GLvoid* bodyOffset = 0; const GLvoid* diskOffset = (GLvoid*) m_bodyIndexCount; glEnableVertexAttribArray(colorSlot); glDrawElements(GL_TRIANGLES, m_bodyIndexCount, GL_UNSIGNED_BYTE, bodyOffset); glDisableVertexAttribArray(colorSlot); glVertexAttrib4f(colorSlot, 1, 1, 1, 1); glDrawElements(GL_TRIANGLES, m_diskIndexCount, GL_UNSIGNED_BYTE, diskOffset); glDisableVertexAttribArray(positionSlot); }
|
That wraps up the tutorial on VBOs; we’ve taken
the Touch Cone sample as far as we can take it!